Now that you understand how to define and invoke generic methods, it’s time to turn your attention to the construction of a generic structure (the process of building a generic class is identical) within a new Console Application project named GenericPoint. Assume you have built a generic Point structure that supports a single type parameter that represents the underlying storage for the (x, y) coordinates. The caller can then create Point<T> types as follows:
// Point using ints. Point<int> p = new Point<int>(10, 10); // Point using double. Point<double> p2 = new Point<double>(5.4, 3.3);
Here is the complete definition of Point<T>, with some analysis to follow:
// A generic Point structure. public struct Point<T> { // Generic state date. private T xPos; private T yPos; // Generic constructor. public Point(T xVal, T yVal) { xPos = xVal; yPos = yVal; } // Generic properties. public T X { get { return xPos; } set { xPos = value; } } public T Y { get { return yPos; } set { yPos = value; } } public override string ToString() { return string.Format("[{0}, {1}]", xPos, yPos); } // Reset fields to the default value of the // type parameter. public void ResetPoint() { xPos = default(T); yPos = default(T); } }
As you can see, Point<T> leverages its type parameter in the definition of the field data, constructor arguments, and property definitions. Notice that, in addition to overriding ToString(), Point<T> defines a method named ResetPoint() that uses some new syntax you have not yet seen:
// The "default" keyword is overloaded in C#. // When used with generics, it represents the default // value of a type parameter. public void ResetPoint() { X = default(T); Y = default(T); }
With the introduction of generics, the C# default keyword has been given a dual identity. In addition to its use within a switch construct, it can also be used to set a type parameter to its default value. This is helpful because a generic type does not know the actual placeholders up front, which means it cannot safely assume what the default value will be. The defaults for a type parameter are as follows:
For Point<T>, you can set the X and Y values to 0 directly because it is safe to assume the caller will supply only numerical data. However, you can also increase the overall flexibility of the generic type by using the default(T) syntax. In any case, you can now exercise the methods of Point<T>:
static void Main(string[] args) { Console.WriteLine("***** Fun with Generic Structures *****\n"); // Point using ints. Point<int> p = new Point<int>(10, 10); Console.WriteLine("p.ToString()={0}", p.ToString()); p.ResetPoint(); Console.WriteLine("p.ToString()={0}", p.ToString()); Console.WriteLine(); // Point using double. Point<double> p2 = new Point<double>(5.4, 3.3); Console.WriteLine("p2.ToString()={0}", p2.ToString()); p2.ResetPoint(); Console.WriteLine("p2.ToString()={0}", p2.ToString()); Console.ReadLine(); }
Here is the output:
***** Fun with Generic Structures ***** p.ToString()=[10, 10] p.ToString()=[0, 0] p2.ToString()=[5.4, 3.3] p2.ToString()=[0, 0]
Source Code You can find the GenericPoint project under the Chapter 10 subdirectory.
Generic classes can be the base class to other classes, which means they can define any number of virtual or abstract methods. However, the derived types must abide by a few rules to ensure that the nature of the generic abstraction flows through. First, if a non-generic class extends a generic class, the derived class must specify a type parameter:
// Assume you have created a custom // generic list class. public class MyList<T> { private List<T> listOfData = new List<T>(); } // Non-generic classes must specify the type // parameter when deriving from a // generic base class. public class MyStringList : MyList<string> {}
Second, if the generic base class defines generic virtual or abstract methods, the derived type must override the generic methods using the specified type parameter:
// A generic class with a virtual method. public class MyList<T> { private List<T> listOfData = new List<T>(); public virtual void Insert(T data) { } } public class MyStringList : MyList<string> { // Must substitute the type parameter used in the // parent class in derived methods. public override void Insert(string data) { } }
Third, if the derived type is generic as well, the child class can (optionally) reuse the type placeholder in its definition. However, be aware that any constraints (see next section) placed on the base class must be honored by the derived type, as in this example:
// Note that we now have a default constructor constraint (see next section). public class MyList<T> where T : new() { private List<T> listOfData = new List<T>(); public virtual void Insert(T data) { } } // Derived type must honor constraints. public class MyReadOnlyList<T> : MyList<T> where T : new() { public override void Insert(T data) { } }
Again, in your day-to-day programming tasks, creating custom generic class hierarchies will most likely not be a common task. Nevertheless, doing so is possible (as long as you abide by the rules).